package nf.graph;

import java.util.AbstractCollection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;

public class Group extends AbstractCollection<Node> implements PropertyBag
{
	private Graph graph;
	private Set<Node> nodes;
	private Set<Node> enclosedNodes;
	private Set<Group> childGroups;
	private Group parentGroup;
	private Map<String, Object> properties;
	
	public Group()
	{
		this(null, null);
	}
	
	Group(Graph graph, Group parentGroup)
	{
		this.nodes = new HashSet<Node>();
		this.enclosedNodes = new HashSet<Node>();
		this.childGroups = new HashSet<Group>();
		this.properties = new HashMap<String, Object>();
		
		setGraph(graph);
		setParentGroup(parentGroup);
	}
	
	public Group copy()
	{
		Group g = createNewInstance();
		g.properties = new HashMap<String, Object>(properties);
		
		return g;
	}
	
	protected Group createNewInstance()
	{
		return new Group();
	}
	
	public Graph getGraph()
	{
		return graph;
	}
	
	public Group getParentGroup()
	{
		return parentGroup;
	}
	
	public Set<Group> getChildGroups()
	{
		return Collections.unmodifiableSet(childGroups);
	}
	
	public Set<Node> getEnclosedNodes()
	{
		return Collections.unmodifiableSet(enclosedNodes);
	}
	
	/**
	 * Adds the child group to this group.
	 * @throws WrongGraphException if g's graph does not match this group's
	 * graph
	 */
	public boolean addChildGroup(Group g)
	{
		Graph otherGraph = g.getGraph();
		Group parent;
		
		if (otherGraph != graph) {
			if (otherGraph == null) {
				g.setGraph(graph);
				otherGraph = graph;
			} else {
				throw new WrongGraphException("Cannot add the child group",
					graph, otherGraph);
			}
		}
		
		parent = g.getParentGroup();
		
		if (parent != null)
			parent.removeChildGroup(g);
		
		g.setParentGroup(this);
		enclosedNodes.addAll(g.getEnclosedNodes());
		return childGroups.add(g);
	}
	
	/**
	 * Removes the child group from this group.
	 */
	public boolean removeChildGroup(Group g)
	{
		if (g.getParentGroup() == this)
			g.setParentGroup(null);
		enclosedNodes.removeAll(g.getEnclosedNodes());
		return childGroups.remove(g);
	}
	
	public Object getProperty(String name)
	{
		return properties.get(name);
	}
	
	public Object setProperty(String name, Object newValue)
	{
		return properties.put(name, newValue);
	}
	
	public boolean encloses(Node n)
	{
		return enclosedNodes.contains(n);
	}
	
	public boolean add(Node n)
	{
		addEnclosed(n);
		if (graph != null)
			graph.addNode(n);
		n.setGroup(this);
		return nodes.add(n);
	}
	
	private void addEnclosed(Node n)
	{
		enclosedNodes.add(n);
		if (parentGroup != null)
			parentGroup.addEnclosed(n);
	}
	
	public void clear()
	{
		nodes.clear();
	}
	
	public boolean contains(Object n)
	{
		return nodes.contains(n);
	}
	
	public Iterator<Node> iterator()
	{
		return nodes.iterator();
	}
	
	public boolean remove(Object n)
	{
		Node node;
		
		removeEnclosed(n);
		if (graph != null)
			graph.removeNode(n);
		if (n instanceof Node) {
			node = (Node) n;
			if (node.getGroup() == this)
				node.setGroup(null);
		}
		return nodes.remove(n);
	}
	
	private void removeEnclosed(Object n)
	{
		enclosedNodes.remove(n);
		if (parentGroup != null)
			parentGroup.removeEnclosed(n);
	}
	
	public int size()
	{
		return nodes.size();
	}
	
	void setGraph(Graph graph)
	{
		if (this.graph != null) {
			throw new IllegalStateException("This group already belongs to a " +
				"graph.");
		}
		this.graph = graph;
		
		for (Group child : childGroups) {
			child.setGraph(graph);
		}
		
		if (graph != null) {
			for (Node node : nodes) {
				graph.addNode(node);
			}
		}
	}
	
	void setParentGroup(Group parentGroup)
	{
		this.parentGroup = parentGroup;
	}
	
	public String toString()
	{
		return String.format("<%s nodes=%s, children=%s>",
			getClass().getName(), nodes, childGroups);
	}
}
